﻿#region

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

using Toolbox.Algorithms;

#endregion

namespace Toolbox.Dynamic
{
    public interface IDynamicInterface
    {
        void AddMethod(string name, Type returnType, params Type[] paramTypes);
        Type CreateType();
    }

    public interface IDynamicClass
    {
        IEnumerable<MethodInfo> InterfaceMethods { get; }
        FieldBuilder AddField(string fieldName, Type fieldType);
        void AddCtor(IDynamicMethodBody body, params Type[] paramTypes);

        void AddMethod(
            IDynamicMethodBody body, string name, Type returnType, params Type[] paramTypes);

        void AddMethod(IDynamicMethodBody body, MethodInfo interfaceMethod);

        void AddMethod(
            IDynamicMethodBody body, Type interfaceType, string name, Type returnType,
            params Type[] paramTypes);

        Type CreateType();
    }

    internal class DynamicClass : IDynamicClass, IDynamicInterface
    {
        private Type _createdType;
        private TypeBuilder _typeBuilder;

        public DynamicClass(TypeBuilder typeBuilder)
        {
            _typeBuilder = Contract.NotNull(typeBuilder, "typeBuilder");
        }

        #region IDynamicClass Members

        public IEnumerable<MethodInfo> InterfaceMethods
        {
            get
            {
                foreach (Type i in _typeBuilder.GetInterfaces()) {
                    foreach (MethodInfo m in i.GetMethods())
                        yield return m;
                }
            }
        }

        FieldBuilder IDynamicClass.AddField(string fieldName, Type fieldType)
        {
            CheckIfCreated();
            Contract.NotEmpty(fieldName, "fieldName");
            Contract.NotNull(fieldType, "fieldType");

            return _typeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Private);
        }

        void IDynamicClass.AddCtor(IDynamicMethodBody body, params Type[] paramTypes)
        {
            CheckIfCreated();
            Contract.NotNull(body, "body");
            paramTypes = paramTypes ?? Type.EmptyTypes;

            ConstructorBuilder ctor = _typeBuilder.DefineConstructor(
                MethodAttributes.Public, CallingConventions.Standard, paramTypes);
            ILGenerator ilg = ctor.GetILGenerator();
            body.Emit(ilg);
        }

        void IDynamicClass.AddMethod(
            IDynamicMethodBody body, string name, Type returnType, params Type[] paramTypes)
        {
            CheckIfCreated();
            Contract.NotNull(body, "body");
            Contract.NotEmpty(name, "name");
            Contract.NotNull(returnType, "returnType");
            paramTypes = paramTypes ?? Type.EmptyTypes;

            AddMethod(
                MethodAttributes.Public | MethodAttributes.Virtual, body, name, returnType,
                paramTypes);
        }

        void IDynamicClass.AddMethod(IDynamicMethodBody body, MethodInfo interfaceMethod)
        {
            CheckIfCreated();
            Contract.NotNull(body, "body");
            Contract.NotNull(interfaceMethod, "interfaceMethod");

            Type[] paramTypes = Map.ToArray(
                interfaceMethod.GetParameters(),
                delegate(ParameterInfo param) { return param.ParameterType; });

            (this as IDynamicClass).AddMethod(
                body, interfaceMethod.DeclaringType, interfaceMethod.Name,
                interfaceMethod.ReturnType, paramTypes);
        }

        void IDynamicClass.AddMethod(
            IDynamicMethodBody body, Type interfaceType, string name, Type returnType,
            params Type[] paramTypes)
        {
            CheckIfCreated();
            Contract.NotNull(body, "body");
            Contract.NotNull(interfaceType, "interfaceType");
            Contract.NotEmpty(name, "name");
            Contract.NotNull(returnType, "returnType");
            paramTypes = paramTypes ?? Type.EmptyTypes;

            List<Type> interfaces = new List<Type>(_typeBuilder.GetInterfaces());
            RuntimeAssert.IsTrue(
                interfaces.Contains(interfaceType),
                string.Format(
                    "Type being constructed does not implement '{0}' interface.", interfaceType.Name));

            MethodInfo overrideMethod = interfaceType.GetMethod(name, paramTypes);
            RuntimeAssert.IsTrue(
                overrideMethod != null,
                string.Format(
                    "Method '{0}' was not found in '{1}' interface.", name, interfaceType.Name));
            RuntimeAssert.IsTrue(
                overrideMethod.ReturnType == returnType,
                string.Format(
                    "Given return type does not match '{0}' method in '{1}' interface.", name,
                    interfaceType.Name));

            MethodAttributes methodAttrs = overrideMethod.Attributes & ~MethodAttributes.Abstract;
            MethodBuilder methodBuilder = AddMethod(methodAttrs, body, name, returnType, paramTypes);

            _typeBuilder.DefineMethodOverride(methodBuilder, overrideMethod);
        }

        public Type CreateType()
        {
            if (_createdType == null)
                _createdType = _typeBuilder.CreateType();
            return _createdType;
        }

        #endregion

        #region IDynamicInterface Members

        void IDynamicInterface.AddMethod(string name, Type returnType, params Type[] paramTypes)
        {
            CheckIfCreated();
            Contract.NotEmpty(name, "name");
            Contract.NotNull(returnType, "returnType");
            paramTypes = paramTypes ?? Type.EmptyTypes;

            MethodAttributes ma = MethodAttributes.Public | MethodAttributes.Abstract |
                                  MethodAttributes.Virtual;
            _typeBuilder.DefineMethod(name, ma, returnType, paramTypes);
        }

        #endregion

        protected void CheckIfCreated()
        {
            if (_createdType != null)
                throw new InvalidOperationException(
                    "Cannot modify class; CreateType() already called.");
        }

        private MethodBuilder AddMethod(
            MethodAttributes methodAttrs, IDynamicMethodBody body, string name, Type returnType,
            params Type[] paramTypes)
        {
            CheckIfCreated();
            Contract.NotNull(body, "body");
            Contract.NotEmpty(name, "name");
            Contract.NotNull(returnType, "returnType");
            paramTypes = paramTypes ?? Type.EmptyTypes;

            MethodBuilder methodBuilder = _typeBuilder.DefineMethod(
                name, methodAttrs, returnType, paramTypes);
            body.Emit(methodBuilder.GetILGenerator());
            return methodBuilder;
        }
    }
}